package com.burgstaller.aGTD;

import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.content.DialogInterface.OnClickListener;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.Menu;
import android.view.Menu.Item;
import android.widget.ExpandableListView;
import android.widget.SimpleExpandableListAdapter;
import android.widget.Toast;

import com.burgstaller.aGTD.sync.AgtdSyncAdapter;
import com.burgstaller.aGTD.sync.AgtdSyncAdapter.AgtdSyncData;
import com.burgstaller.aGTD.sync.AgtdSyncAdapter.SyncRecord;
import com.burgstaller.aGTD.sync.AgtdSyncAdapter.RemoteSyncRecord;
import com.burgstaller.aGTD.utils.AgtdSyncObserver;
import com.burgstaller.provider.AGtd;
import com.google.android.collect.Lists;

import de.trantor.mail.MailException;

public class SyncView extends Activity {
	private static final int MSG_LOCAL_SCAN_START = 0;
	private static final int MSG_LOCAL_SCAN_PROGRESS = 1;
	private static final int MSG_LOCAL_SCAN_END = 2;
	private static final int MSG_REMOTE_SCAN_START = 3;
	private static final int MSG_REMOTE_SCAN_PROGRESS = 4;
	private static final int MSG_REMOTE_SCAN_END = 5;
	private static final int MSG_APPLY_LOCALCHANGES_START = 6;
	private static final int MSG_APPLY_LOCALCHANGES_PROGRESS = 7;
	private static final int MSG_APPLY_LOCALCHANGES_END = 8;
	private static final int MSG_APPLY_REMOTECHANGES_START = 9;
	private static final int MSG_APPLY_REMOTECHANGES_PROGRESS = 10;
	private static final int MSG_APPLY_REMOTECHANGES_END = 11;
	private static final int MSG_SYNC_DONE = 12;
	private static final int MSG_SCAN_DONE = 13;
	private static final int MSG_SYNC_ERROR = 99;

	
	private static final String SYNC_GROUP = "GROUP";
	private static final String SYNC_EXPLAIN = "EXPLAIN";
	
	private static final int APPLY_ID = Menu.FIRST;
	
	private Thread m_syncThread;
	private ProgressDialog m_syncProgress;
	
	private List<Map> m_groupData;
	private List<List<Map>> m_childData;

	private Handler m_progressHandler = new Handler() {
		private boolean m_syncDone;

		public void handleMessage(android.os.Message msg) {
			try {
				switch (msg.what) {
				case MSG_LOCAL_SCAN_START:
					getProgressDlg("Scanning local changes");
					break;
				case MSG_REMOTE_SCAN_START:
					getProgressDlg("Scanning remote changes");
					break;
				case MSG_REMOTE_SCAN_END:
					updateData();
					checkStaleRecords();
					break;
				case MSG_APPLY_LOCALCHANGES_START:
					getProgressDlg("Applying local changes");
					break;
				case MSG_APPLY_REMOTECHANGES_START:
					getProgressDlg("Applying remote changes");
					break;
				case MSG_APPLY_REMOTECHANGES_END:
					break;
				case MSG_APPLY_LOCALCHANGES_END:
					m_syncDone = true;
					break;
					
				case MSG_SCAN_DONE:
				case MSG_SYNC_DONE: 
					if (m_syncProgress != null) {
						m_syncProgress.dismiss();
						m_syncProgress = null;
					}
					if (m_syncDone) {
						finish();
					}
					break;
				case MSG_SYNC_ERROR:
					Exception errorExcp = (Exception) msg.obj;
					AlertDialog.show(SyncView.this, 
							getText(R.string.error_sync_viewtitle), -1, 
							errorExcp.getMessage(), 
							getText(R.string.button_ok), false);
					break;
				}
			} catch (Exception e) {
				Log.e(AGtd.LOG_TAG, "Handler", e);
			}
		}

	};

	ExpandableListView m_listView;
	private AgtdSyncData m_localChanges;
	private AgtdSyncData m_remoteChanges;
	private AgtdSyncAdapter m_adapter;
	private SimpleExpandableListAdapter m_listAdapter;
	private boolean m_haveChanges;

	private void getProgressDlg(String msg) {
		if (m_syncProgress == null) {
			m_syncProgress = ProgressDialog.show(SyncView.this, "Sync", msg);
		} else {
			m_syncProgress.setMessage(msg);
		}
	}

	@Override
	protected void onCreate(Bundle icicle) {
		super.onCreate(icicle);
		setTitle(R.string.viewtitle_sync);
		setContentView(R.layout.sync);
		m_listView = (ExpandableListView) findViewById(R.id.synclist);
		m_groupData = Lists.newArrayList();
		m_childData = Lists.newArrayList();
        // Set up our adapter
        m_listAdapter = new SimpleExpandableListAdapter(
                this,
                m_groupData,
              android.R.layout.simple_expandable_list_item_2,
              new String[] { SYNC_GROUP, SYNC_EXPLAIN },
              new int[] { android.R.id.text1, android.R.id.text2 },
                m_childData,
                R.layout.synclist,
                new String[] { SYNC_GROUP, SYNC_EXPLAIN },
                new int[] { android.R.id.text1, android.R.id.text2 }
                );

        m_listView.setAdapter(m_listAdapter);
        startSync();
	}
	
	
	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		super.onCreateOptionsMenu(menu);
		menu.add(0, APPLY_ID, R.string.sync_apply);
		return true;
	}
	
	@Override
	public boolean onOptionsItemSelected(Item item) {
		if (item.getId() == APPLY_ID) {
			startCommit();
			return true;
		}
		return super.onOptionsItemSelected(item);
	}

	void checkStaleRecords() {
		if (m_remoteChanges != null && m_remoteChanges.hasStaleRecords()) {
			StringBuilder builder = new StringBuilder(getText(
					R.string.sync_stale_description));
			builder.append("\n");
			for (SyncRecord record : m_remoteChanges.getStaleRecords()) {
				builder.append("* ");
				builder.append(record.m_title);
				builder.append("\n");
			}
			SyncView.this.showAlert( 
				getText(R.string.sync_stale_viewtitle), -1, 
				builder.toString(), "Delete", 
				new OnClickListener() {
					public void onClick(DialogInterface dialog, 
							int whichButton) {
						m_localChanges.getDeleted().addAll(
								m_remoteChanges.getStaleRecords());
						updateData();
					}
				}, "Keep/Download",  
				new OnClickListener() {
					public void onClick(DialogInterface arg0, int whichButton) {
						m_remoteChanges.getAdded().addAll(
								m_remoteChanges.getStaleRecords());
						updateData();
					}
					
				}, null, null, false, null);
		}

	}
	private void startSync() {
		if (m_syncThread == null) {
			ContentResolver resolver = getContentResolver();
			resolver.registerSyncObserver(new AgtdSyncObserver(true));
			resolver.startSync(AGtd.Tasks.CONTENT_URI, null);
			m_syncThread = new Thread() {
				@Override
				public void run() {
					super.run();
					scanForChanges();
				}
			};
			m_syncThread.start();
		}
	}

	
	/**
	 * this is a thread worker method that will scan the server for changes.
	 * It is meant to be run in a seperate thread.
	 */
	private synchronized void scanForChanges() {
		Looper.prepare();
		ContentResolver resolver = getContentResolver();

		Exception errorExcp = null;
		try {
			doScanChanges();
		} catch (Exception e) {
			Log.e(AGtd.LOG_TAG, "Error synching: ", e);
			e.printStackTrace();
			// there was some problem with the server.
			errorExcp = e;
			m_progressHandler.obtainMessage(MSG_SYNC_ERROR, errorExcp).sendToTarget();
		} finally {
			if (m_adapter != null) {
				m_adapter.closeClient();
			}
			m_progressHandler.sendMessage(m_progressHandler.obtainMessage(
					MSG_SCAN_DONE));

			m_syncThread = null;
		}
	}




	private void doScanChanges() throws Exception, IOException, MailException {
		m_adapter = new AgtdSyncAdapter(this);
		m_progressHandler.sendMessage(m_progressHandler
				.obtainMessage(MSG_LOCAL_SCAN_START));

		m_localChanges = (AgtdSyncData) m_adapter
				.readSyncData(null);
		m_progressHandler.sendMessage(m_progressHandler
				.obtainMessage(MSG_LOCAL_SCAN_END));

		Log.i(AGtd.LOG_TAG, "local changes: " + m_localChanges.toString());
		Log.i(AGtd.LOG_TAG, "\n\n");

		m_progressHandler.sendMessage(m_progressHandler
				.obtainMessage(MSG_REMOTE_SCAN_START));
		m_adapter.openClient();
		m_remoteChanges = (AgtdSyncData) m_adapter
				.getServerDiffs(null, null, AGtd.Tasks.CONTENT_URI, null);
		m_progressHandler.sendMessage(m_progressHandler
				.obtainMessage(MSG_REMOTE_SCAN_END));
		Log.i(AGtd.LOG_TAG, "server changes: " + m_remoteChanges.toString());
		m_haveChanges = true;
	}



	private void startCommit() {
		if (!m_haveChanges) {
			Toast.makeText(this, R.string.sync_error_nothing_to_apply, 
					Toast.LENGTH_SHORT).show();
			return;
		}
		if (m_syncThread == null) {
			m_syncThread = new Thread() {
				@Override
				public void run() {
					super.run();
					applyChanges();
				}
			};
			m_syncThread.start();
		}
	}


	/**
	 * this is a thread method that will apply the changes that
	 * were found before. This method is designed to be run in a seperate 
	 * thread.
	 */
	private synchronized void applyChanges() {
		Looper.prepare();
		try {
			m_adapter.openClient();
			doApplyChanges();
		} catch (Exception e) {
			Log.e(AGtd.LOG_TAG, "Error applying: ", e);
			e.printStackTrace();
			// there was some problem with the server.
			m_progressHandler.obtainMessage(MSG_SYNC_ERROR, e)
					.sendToTarget();
		} finally {
			if (m_adapter != null) {
				m_adapter.closeClient();
			}
			m_progressHandler.sendMessage(m_progressHandler
					.obtainMessage(MSG_SYNC_DONE));

			m_syncThread = null;
		}
		m_haveChanges = false;
	}


	private void doApplyChanges() {

		m_progressHandler.sendMessage(m_progressHandler.obtainMessage(
				MSG_APPLY_LOCALCHANGES_START));
		Enumeration enumAdded = m_remoteChanges.getAdded().elements();
		while (enumAdded.hasMoreElements()) {
			RemoteSyncRecord record = (RemoteSyncRecord) enumAdded
					.nextElement();
			Uri uri = getContentResolver().insert(AGtd.Tasks.CONTENT_URI,
					m_adapter.getContentValuesFor(record));
			record.m_localId = Integer.parseInt(uri.getLastPathSegment());
			// records that are added locally must be uploaded so
			// that all its meta infos are uploaded to the server
			m_localChanges.getModified().add(record);
		}

		m_progressHandler.sendMessage(m_progressHandler.obtainMessage(
				MSG_APPLY_REMOTECHANGES_START));
		m_adapter.sendClientDiffs(m_localChanges);
		m_progressHandler.sendMessage(m_progressHandler.obtainMessage(
				MSG_APPLY_REMOTECHANGES_END));

		Enumeration enumChanged = m_remoteChanges.getModified().elements();
		while (enumChanged.hasMoreElements()) {
			RemoteSyncRecord record = (RemoteSyncRecord) enumChanged
					.nextElement();
			// sanity check, make sure that the remote changes are newer
			long localChangeDate = -1;
			Cursor localCursor = m_adapter.findFullTaskById(record.m_localId);
			if (localCursor.first()) {
				int sync_state = localCursor.getInt(
						m_adapter.FULL_SYNC_STATUS_INDEX);
				long modDate = localCursor.getLong(
						m_adapter.FULL_MODIFIFIED_INDEX);
				int delta = 30000; 	// tolerance to make sure that 
									// clock differences on the server dont
									// create false changes
				String msgDate = record.m_newMessage.getHeaderValue("Date");
				if (msgDate != null) {
					Date messageDate;
					try {
						messageDate = DateFormat.getDateTimeInstance(
								DateFormat.FULL, DateFormat.FULL).parse(
								msgDate);
					} catch (ParseException e) {
						Log.e(AGtd.LOG_TAG, "Error parsing message date, " +
								"assuming the message is new", e);
						e.printStackTrace();
						messageDate = new Date();
					}
					long lngDate = messageDate.getTime();
					if ((lngDate - delta) > modDate && sync_state == AGtd.Tasks.SYNC_STATUS_SYNCED) {
						// ok, we really should apply the change.
						getContentResolver().update(AGtd.Tasks.CONTENT_URI,
								m_adapter.getContentValuesFor(record),
								AGtd.Tasks._ID + " = ?",
								new String[] { "" + record.m_localId });
					} else {
						// otherwise mark the task as locally modified, so
						// that next time it will be synced upwards
						ContentValues statusUpdates = new ContentValues();
						statusUpdates.put(AGtd.Tasks.SYNC_STATUS, AGtd.Tasks.SYNC_STATUS_MODIFIED);
						statusUpdates.put(AGtd.Tasks.MODIFIED_DATE, System.currentTimeMillis());
						getContentResolver().update(AGtd.Tasks.CONTENT_URI,
								statusUpdates,
								AGtd.Tasks._ID + " = ?",
								new String[] { "" + record.m_localId });							
					}
				}
			}
		}

		Enumeration enumDeleted = m_remoteChanges.getDeleted().elements();
		// now all the records that were deleted remotely, will be marked as done
		while (enumDeleted.hasMoreElements()) {
			RemoteSyncRecord record = (RemoteSyncRecord) enumDeleted
					.nextElement();
			ContentValues values = m_adapter.getContentValuesFor(record);
			values.put(AGtd.Tasks.COMPLETED, 1);
			getContentResolver().update(AGtd.Tasks.CONTENT_URI, values,
					AGtd.Tasks._ID + " = ?",
					new String[] { "" + record.m_localId });
		}
		
		// now we can also delete the locally deleted records
		// as the changes have been synched
		for (SyncRecord record : (Vector<SyncRecord>)
				m_localChanges.getDeleted()) {
			getContentResolver().delete(AGtd.Tasks.CONTENT_URI, 
					AGtd.Tasks._ID + " =?", new String[] {
					"" + record.m_localId});
		}
		m_progressHandler.sendMessage(m_progressHandler
				.obtainMessage(MSG_APPLY_LOCALCHANGES_END));
	}

	@SuppressWarnings("unchecked")
	private void updateData() {
		m_groupData.clear();
		m_childData.clear();
		
		int localCount = m_localChanges.getAdded().size() 
			+ m_localChanges.getModified().size() 
			+ m_localChanges.getDeleted().size();
		int remoteCount = m_remoteChanges.getAdded().size() 
			+ m_remoteChanges.getModified().size()
			+ m_remoteChanges.getDeleted().size();
		Map localMods = new HashMap<String, String>();
		localMods.put(SYNC_GROUP, "Local Modifications");
		localMods.put(SYNC_EXPLAIN, "To be uploaded: " + localCount);
		m_groupData.add(localMods);
		Map remoteMods = new HashMap<String, String>();
		remoteMods.put(SYNC_GROUP, "Remote Modifications");
		remoteMods.put(SYNC_EXPLAIN, "To be applied locally: " + remoteCount);
		m_groupData.add(remoteMods);
		
		List<Map> localChanges = Lists.newArrayList();
		for (SyncRecord record : (Vector<SyncRecord>) m_localChanges.getAdded()) {
			Map add = new HashMap<String, String>();
			add.put(SYNC_GROUP, record.m_title);
			add.put(SYNC_EXPLAIN, "Added");
			localChanges.add(add);
		}
		
		for (SyncRecord record : (Vector<SyncRecord>) m_localChanges.getModified()) {
			Map add = new HashMap<String, String>();
			add.put(SYNC_GROUP, record.m_title);
			add.put(SYNC_EXPLAIN, "Modified");
			localChanges.add(add);
		}

		for (SyncRecord record : (Vector<SyncRecord>) m_localChanges.getDeleted()) {
			Map add = new HashMap<String, String>();
			add.put(SYNC_GROUP, record.m_title);
			add.put(SYNC_EXPLAIN, "deleted");
			localChanges.add(add);
		}
		m_childData.add(localChanges);

		
		List<Map>remoteChanges = Lists.newArrayList();
		for (SyncRecord record : m_remoteChanges.getAdded()) {
			Map add = new HashMap<String, String>();
			add.put(SYNC_GROUP, record.m_title);
			add.put(SYNC_EXPLAIN, "To be added");
			remoteChanges.add(add);
		}
		
		for (SyncRecord record : m_remoteChanges.getModified()) {
			Map add = new HashMap<String, String>();
			add.put(SYNC_GROUP, record.m_title);
			add.put(SYNC_EXPLAIN, "to be changed");
			remoteChanges.add(add);
		}

		for (SyncRecord record : m_remoteChanges.getDeleted()) {
			Map add = new HashMap<String, String>();
			add.put(SYNC_GROUP, record.m_title);
			add.put(SYNC_EXPLAIN, "to be completed");
			remoteChanges.add(add);
		}
		m_childData.add(remoteChanges);


		m_listAdapter.notifyDataSetChanged();
	}
}
